package weka.core.pmml;

import java.util.ArrayList;

import weka.core.Attribute;

/**
 * Built-in function for uppercase, substring and trimblanks.
 * 
 * @author Mark Hall (mhall{[at]}pentaho{[dot]}com
 * @version $Revision 1.0 $
 */
public class BuiltInString extends Function {

    /**
     * For serialization
     */
    private static final long serialVersionUID = -7391516909331728653L;

    /**
     * Enum for the string functions
     */
    enum StringFunc {
        UPPERCASE("uppercase") {
            String eval(Object[] args) {
                return ((String) args[0]).toUpperCase();
            }

            boolean legalNumParams(int num) {
                return (num == 1);
            }

            String[] getParameterNames() {
                return new String[] { "input" };
            }
        },
        SUBSTRING("substring") {
            String eval(Object[] args) {
                String input = (String) args[0];
                int startPos = ((Integer) args[1]).intValue();
                int length = ((Integer) args[2]).intValue();
                return input.substring(startPos - 1, startPos + length);
            }

            boolean legalNumParams(int num) {
                return (num == 3);
            }

            String[] getParameterNames() {
                return new String[] { "input", "startPos", "length" };
            }
        },
        TRIMBLANKS("trimBlanks") {
            String eval(Object[] args) {
                return ((String) args[0]).trim();
            }

            boolean legalNumParams(int num) {
                return (num == 1);
            }

            String[] getParameterNames() {
                return new String[] { "input" };
            }
        };

        abstract String eval(Object[] args);

        abstract boolean legalNumParams(int num);

        abstract String[] getParameterNames();

        private String m_stringVal;

        StringFunc(String funcName) {
            m_stringVal = funcName;
        }

        public String toString() {
            return m_stringVal;
        }
    }

    /** The function to apply */
    protected StringFunc m_func;

    /** The output structure produced by this function */
    protected Attribute m_outputDef = null;

    BuiltInString(StringFunc func) {
        m_func = func;
        m_functionName = m_func.toString();
    }

    /**
     * Get the structure of the result produced by this function. Subclasses must
     * implement.
     * 
     * @return the structure of the result produced by this function.
     */
    public Attribute getOutputDef() {

        if (m_outputDef == null) {
            if (m_func == StringFunc.SUBSTRING) {
                // there is no way we can compute the legal values for this attribute
                // in advance of the application of this function. So return a string attribute
                m_outputDef = new Attribute("BuiltInStringResult:substring", (ArrayList<String>) null);
            }
            // for the other functions we can compute the resulting set of values
            Attribute inputVals = m_parameterDefs.get(0);
            ArrayList<String> newVals = new ArrayList<String>();
            for (int i = 0; i < inputVals.numValues(); i++) {
                String inVal = inputVals.value(i);
                newVals.add(m_func.eval(new Object[] { inVal }));
            }
            m_outputDef = new Attribute("BuiltInStringResult:" + m_func.toString(), newVals);
        }

        return m_outputDef;
    }

    /**
     * Returns an array of the names of the parameters expected as input by this
     * function. May return null if the function can accept an unbounded number of
     * arguments.
     * 
     * @return an array of the parameter names (or null if the function can accept
     *         any number of arguments).
     */
    public String[] getParameterNames() {
        return m_func.getParameterNames();
    }

    private Object[] setUpArgs(double[] incoming) {
        // construct the input to the function
        Object[] args = new Object[incoming.length];
        Attribute input = m_parameterDefs.get(0);
        args[0] = input.value((int) incoming[0]);
        for (int i = 1; i < incoming.length; i++) {
            args[i] = new Integer((int) incoming[i]);
        }

        return args;
    }

    /**
     * Get the result of applying this function.
     * 
     * @param incoming the arguments to this function (supplied in order to match
     *                 that of the parameter definitions
     * @return the result of applying this function. When the optype is categorical
     *         or ordinal, an index into the values of the output definition is
     *         returned.
     * @throws Exception if there is a problem computing the result of this function
     */
    public double getResult(double[] incoming) throws Exception {

        if (m_parameterDefs == null) {
            throw new Exception("[BuiltInString] incoming parameter structure has not been set");
        }

        if (!m_func.legalNumParams(incoming.length)) {
            throw new Exception("[BuiltInString] wrong number of parameters!");
        }

        // construct the input to the function
        Object[] args = setUpArgs(incoming);

        // get the result
        String result = m_func.eval(args);
        int resultI = m_outputDef.indexOfValue(result);
        if (resultI < 0) {
            if (m_outputDef.isString()) {
                // add this as a new value
                resultI = m_outputDef.addStringValue(result);
            } else {
                throw new Exception("[BuiltInString] unable to find value " + result + " in nominal result type!");
            }
        }

        return resultI;
    }

    /**
     * Get the result of applying this function when the output type categorical.
     * Will throw an exception for numeric output. If subclasses output definition
     * is a string attribute (i.e. because all legal values can't be computed
     * apriori), then the subclass will need to overide this method and return
     * something sensible in this case.
     * 
     * @param incoming the incoming arguments to this function (supplied in order to
     *                 match that of the parameter definitions
     * @return the result of applying this function as a String.
     * @throws Exception if this method is not applicable because the optype is not
     *                   categorical/ordinal
     *
     *                   public String getResultCategorical(double[] incoming)
     *                   throws Exception { if (m_parameterDefs == null) { throw new
     *                   Exception("[BuiltInString] incoming parameter structure has
     *                   not been set"); }
     * 
     *                   if (!m_func.legalNumParams(incoming.length)) { throw new
     *                   Exception("[BuiltInString] wrong number of parameters!"); }
     * 
     *                   // construct the input to the function Object[] args =
     *                   setUpArgs(incoming);
     * 
     *                   // get the result String result = m_func.eval(args);
     * 
     *                   return result; }
     */

    /**
     * Set the structure of the parameters that are expected as input by this
     * function. This must be called before getOutputDef() is called.
     * 
     * @param paramDefs the structure of the input parameters
     * @throws Exception if the number or types of parameters are not acceptable by
     *                   this function
     */
    public void setParameterDefs(ArrayList<Attribute> paramDefs) throws Exception {

        m_parameterDefs = paramDefs;

        if (!m_func.legalNumParams(m_parameterDefs.size())) {
            throw new Exception("[BuiltInMath] illegal number of parameters for function: " + m_functionName);
        }
    }

    public String toString() {
        String result = m_func.toString() + "(";
        for (int i = 0; i < m_parameterDefs.size(); i++) {
            result += m_parameterDefs.get(i).name();
            if (i != m_parameterDefs.size() - 1) {
                result += ", ";
            } else {
                result += ")";
            }
        }
        return result;
    }

}
